package jcore.jsonrpc.common;

import java.text.ParseException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;

/***
 * 异步对象的转换
 * @author just
 *
 */
public class JSONObject {

    /**
     * JSONObject.NULL is equivalent to the value that JavaScript calls null,
     * whilst Java's null is equivalent to the value that JavaScript calls
     * undefined.
     */
     public static final class Null {

        /**
         * Make a Null object.
         */
        private Null() {
        }


        /**
         * There is only intended to be a single instance of the NULL object,
         * so the clone method returns itself.
         * @return     NULL.
         */
        protected final Object clone() {
            return this;
        }


        /**
         * A Null object is equal to the null value and to itself.
         * @param object    An object to test for nullness.
         * @return true if the object parameter is the JSONObject.NULL object
         *  or null.
         */
        public boolean equals(Object object) {
            return object == null || object == this;
        }


        /**
         * Get the "null" string value.
         * @return The string "null".
         */
        public String toString() {
            return "null";
        }
    }


    /**
     * The hash map where the JSONObject's properties are kept.
     */
    private Map myHashMap;

    /**
     * It is sometimes more convenient and less ambiguous to have a NULL
     * object than to use Java's null value.
     * JSONObject.NULL.equals(null) returns true.
     * JSONObject.NULL.toString() returns "null".
     */
    public static final Object NULL = new Null();

    /**
     * Construct an empty JSONObject.
     */
    public JSONObject() {
        myHashMap = Collections.synchronizedMap(new HashMap());
    }


    /**
     * Construct a JSONObject from a JSONTokener.
     * @throws ParseException if there is a syntax error in the source string.
     * @param x A JSONTokener object containing the source string.
     */
    public JSONObject(JSONTokener x) throws ParseException {
        this();
        char c;
        String key;
        if (x.next() == '%') {
            x.unescape();
        }
        x.back();
        if (x.nextClean() != '{') {
            throw x.syntaxError("A JSONObject must begin with '{'");
        }
        while (true) {
            c = x.nextClean();
            switch (c) {
            case 0:
                throw x.syntaxError("A JSONObject must end with '}'");
            case '}':
                return;
            default:
                x.back();
                key = x.nextValue().toString();
            }
            if (x.nextClean() != ':') {
                throw x.syntaxError("Expected a ':' after a key");
            }
            myHashMap.put(key, x.nextValue());
            switch (x.nextClean()) {
            case ',':
                if (x.nextClean() == '}') {
                    return;
                }
                x.back();
                break;
            case '}':
                return;
            default:
                throw x.syntaxError("Expected a ',' or '}'");
            }
        }
    }


    /**
     * Construct a JSONObject from a string.
     * @exception ParseException The string must be properly formatted.
     * @param string    A string beginning with '{' and ending with '}'.
     */
    public JSONObject(String string) throws ParseException {
        this(new JSONTokener(string));
    }


    /**
     * Construct a JSONObject from a Map.
     * @param map A map object that can be used to initialize the contents of
     *  the JSONObject.
     */
    public JSONObject(Map map) {
        myHashMap = new HashMap(map);
    }


    /**
     * Accumulate values under a key. It is similar to the put method except
     * that if there is already an object stored under the key then a
     * JSONArray is stored under the key to hold all of the accumulated values.
     * If there is already a JSONArray, then the new value is appended to it.
     * In contrast, the put method replaces the previous value.
     * @throws NullPointerException if the key is null
     * @param key   A key string.
     * @param value An object to be accumulated under the key.
     * @return this.
     */
    public JSONObject accumulate(String key, Object value)
            throws NullPointerException {
        JSONArray a;
        Object o = opt(key);
        if (o == null) {
            put(key, value);
        } else if (o instanceof JSONArray) {
            a = (JSONArray)o;
            a.put(value);
        } else {
            a = new JSONArray();
            a.put(o);
            a.put(value);
            put(key, a);
        }
        return this;
    }


    /**
     * Get the value object associated with a key.
     * @exception NoSuchElementException if the key is not found.
     *
     * @param key   A key string.
     * @return      The object associated with the key.
     */
    public Object get(String key) throws NoSuchElementException {
        Object o = opt(key);
        return o;
    }


    /**
     * Get the boolean value associated with a key.
     * @exception NoSuchElementException if the key is not found.
     * @exception ClassCastException
     *  if the value is not a Boolean or the String "true" or "false".
     *
     * @param key   A key string.
     * @return      The truth.
     */
    public boolean getBoolean(String key)
            throws ClassCastException, NoSuchElementException {
        Object o = get(key);
        if (o == Boolean.FALSE || o.equals("false")) {
            return false;
        } else if (o == Boolean.TRUE || o.equals("true")) {
            return true;
        }
        throw new ClassCastException("JSONObject[" +
            quote(key) + "] is not a Boolean.");
    }


    /**
     * Get the double value associated with a key.
     * @exception NoSuchElementException if the key is not found or
     *  if the value is a Number object.
     * @exception NumberFormatException if the value cannot be converted to a
     *  number.
     * @param key   A key string.
     * @return      The numeric value.
     */
    public double getDouble(String key)
            throws NoSuchElementException, NumberFormatException {
        Object o = get(key);
        if (o instanceof Number) {
            return ((Number)o).doubleValue();
        }
        if (o instanceof String) {
            return new Double((String)o).doubleValue();
        }
        throw new NumberFormatException("JSONObject[" +
            quote(key) + "] is not a number.");
    }


    /**
     * Get the HashMap the holds that contents of the JSONObject.
     * @return The getHashMap.
     */
     public Map getHashMap() {
        return myHashMap;
     }


    /**
     * Get the int value associated with a key.
     * @exception NoSuchElementException if the key is not found
     * @exception NumberFormatException
     *  if the value cannot be converted to a number.
     *
     * @param key   A key string.
     * @return      The integer value.
     */
    public int getInt(String key)
            throws NoSuchElementException, NumberFormatException {
        Object o = get(key);
        if (o instanceof Number) {
            return ((Number)o).intValue();
        }
        return (int)getDouble(key);
    }


    /**
     * Get the long value associated with a key.
     * @exception NoSuchElementException if the key is not found
     * @exception NumberFormatException
     *  if the value cannot be converted to a number.
     *
     * @param key   A key string.
     * @return      The long value.
     */
    public long getLong(String key)
            throws NoSuchElementException, NumberFormatException {
        Object o = get(key);
        if (o instanceof Number) {
            return ((Number)o).longValue();
        }
        return (long)getDouble(key);
    }


    /**
     * Get the JSONArray value associated with a key.
     * @exception NoSuchElementException if the key is not found or
     *  if the value is not a JSONArray.
     *
     * @param key   A key string.
     * @return      A JSONArray which is the value.
     */
    public JSONArray getJSONArray(String key) throws NoSuchElementException {
        Object o = get(key);
        if (o instanceof JSONArray) {
            return (JSONArray)o;
        }
        throw new NoSuchElementException("JSONObject[" +
            quote(key) + "] is not a JSONArray.");
    }


    /**
     * Get the JSONObject value associated with a key.
     * @exception NoSuchElementException if the key is not found or
     *  if the value is not a JSONObject.
     *
     * @param key   A key string.
     * @return      A JSONObject which is the value.
     */
    public JSONObject getJSONObject(String key) throws NoSuchElementException {
        Object o = get(key);
        if (o instanceof JSONObject) {
            return (JSONObject)o;
        }
        throw new NoSuchElementException("JSONObject[" +
            quote(key) + "] is not a JSONObject.");
    }


    /**
     * Get the string associated with a key.
     * @exception NoSuchElementException if the key is not found.
     *
     * @param key   A key string.
     * @return      A string which is the value.
     */
    public String getString(String key) throws NoSuchElementException {
        return get(key).toString();
    }


    /**
     * Determine if the JSONObject contains a specific key.
     * @param key   A key string.
     * @return      true if the key exists in the JSONObject.
     */
    public boolean has(String key) {
        return myHashMap.containsKey(key);
    }


    /**
     * Determine if the value associated with the key is null or if there is
     *  no value.
     * @param key   A key string.
     * @return      true if there is no value associated with the key or if
     *  the value is the JSONObject.NULL object.
     */
    public boolean isNull(String key) {
        return JSONObject.NULL.equals(opt(key));
    }


    /**
     * Get an enumeration of the keys of the JSONObject.
     *
     * @return An iterator of the keys.
     */
    public Iterator keys() {
        return myHashMap.keySet().iterator();
    }


    /**
     * Get the number of keys stored in the JSONObject.
     *
     * @return The number of keys in the JSONObject.
     */
    public int length() {
        return myHashMap.size();
    }


    /**
     * Produce a JSONArray containing the names of the elements of this
     * JSONObject.
     * @return A JSONArray containing the key strings, or null if the JSONObject
     * is empty.
     */
    public JSONArray names() {
        JSONArray ja = new JSONArray();
        Iterator  keys = keys();
        while (keys.hasNext()) {
            ja.put(keys.next());
        }
        if (ja.length() == 0) {
            return null;
        }
        return ja;
    }


    /**
     * Produce a string from a number.
     * @exception ArithmeticException JSON can only serialize finite numbers.
     * @param  n A Number
     * @return A String.
     */
    static public String numberToString(Number n) throws ArithmeticException {
        if (
                (n instanceof Float &&
                    (((Float)n).isInfinite() || ((Float)n).isNaN())) ||
                (n instanceof Double &&
                    (((Double)n).isInfinite() || ((Double)n).isNaN()))) {
            throw new ArithmeticException(
                "JSON can only serialize finite numbers.");
        }

// Shave off trailing zeros and decimal point, if possible.

        String s = n.toString().toLowerCase();
        if (s.indexOf('e') < 0 && s.indexOf('.') > 0) {
            while (s.endsWith("0")) {
                s = s.substring(0, s.length() - 1);
            }
            if (s.endsWith(".")) {
                s = s.substring(0, s.length() - 1);
            }
        }
        return s;
    }


    /**
     * Get an optional value associated with a key.
     * @exception NullPointerException  The key must not be null.
     * @param key   A key string.
     * @return      An object which is the value, or null if there is no value.
     */
    public Object opt(String key) throws NullPointerException {
        if (key == null) {
            throw new NullPointerException("Null key");
        }
        return myHashMap.get(key);
    }


    /**
     * Get an optional boolean associated with a key.
     * It returns false if there is no such key, or if the value is not
     * Boolean.TRUE or the String "true".
     *
     * @param key   A key string.
     * @return      The truth.
     */
    public boolean optBoolean(String key) {
        return optBoolean(key, false);
    }


    /**
     * Get an optional boolean associated with a key.
     * It returns the defaultValue if there is no such key, or if it is not
     * a Boolean or the String "true" or "false".
     *
     * @param key              A key string.
     * @param defaultValue     The default.
     * @return      The truth.
     */
    public boolean optBoolean(String key, boolean defaultValue) {
        Object o = opt(key);
        if (o != null) {
            if (o == Boolean.FALSE || o.equals("false")) {
                return false;
            } else if (o == Boolean.TRUE || o.equals("true")) {
                return true;
            }
        }
        return defaultValue;
    }


    /**
     * Get an optional double associated with a key,
     * or NaN if there is no such key or if its value is not a number.
     * If the value is a string, an attempt will be made to evaluate it as
     * a number.
     *
     * @param key   A string which is the key.
     * @return      An object which is the value.
     */
    public double optDouble(String key)  {
        return optDouble(key, Double.NaN);
    }


    /**
     * Get an optional double associated with a key, or the
     * defaultValue if there is no such key or if its value is not a number.
     * If the value is a string, an attempt will be made to evaluate it as
     * a number.
     *
     * @param key   A key string.
     * @param defaultValue     The default.
     * @return      An object which is the value.
     */
    public double optDouble(String key, double defaultValue)  {
        Object o = opt(key);
        if (o != null) {
            if (o instanceof Number) {
                return ((Number)o).doubleValue();
            }
            try {
                return new Double((String)o).doubleValue();
            }
            catch (Exception e) {
            }
        }
        return defaultValue;
    }


    /**
     * Get an optional int value associated with a key,
     * or zero if there is no such key or if the value is not a number.
     * If the value is a string, an attempt will be made to evaluate it as
     * a number.
     *
     * @param key   A key string.
     * @return      An object which is the value.
     */
    public int optInt(String key) {
        return optInt(key, 0);
    }


    /**
     * Get an optional int value associated with a key,
     * or the default if there is no such key or if the value is not a number.
     * If the value is a string, an attempt will be made to evaluate it as
     * a number.
     *
     * @param key   A key string.
     * @param defaultValue     The default.
     * @return      An object which is the value.
     */
    public int optInt(String key, int defaultValue) {
        Object o = opt(key);
        if (o != null) {
            if (o instanceof Number) {
                return ((Number)o).intValue();
            }
            try {
                return Integer.parseInt((String)o);
            } catch (Exception e) {
            }
        }
        return defaultValue;
    }


    /**
     * Get an optional JSONArray associated with a key.
     * It returns null if there is no such key, or if its value is not a
     * JSONArray.
     *
     * @param key   A key string.
     * @return      A JSONArray which is the value.
     */
    public JSONArray optJSONArray(String key) {
        Object o = opt(key);
        if (o instanceof JSONArray) {
            return (JSONArray) o;
        }
        return null;
    }


    /**
     * Get an optional JSONObject associated with a key.
     * It returns null if there is no such key, or if its value is not a
     * JSONObject.
     *
     * @param key   A key string.
     * @return      A JSONObject which is the value.
     */
    public JSONObject optJSONObject(String key) {
        Object o = opt(key);
        if (o instanceof JSONObject) {
            return (JSONObject)o;
        }
        return null;
    }


    /**
     * Get an optional string associated with a key.
     * It returns an empty string if there is no such key. If the value is not
     * a string and is not null, then it is coverted to a string.
     *
     * @param key   A key string.
     * @return      A string which is the value.
     */
    public String optString(String key) {
        return optString(key, "");
    }


    /**
     * Get an optional string associated with a key.
     * It returns the defaultValue if there is no such key.
     *
     * @param key   A key string.
     * @param defaultValue     The default.
     * @return      A string which is the value.
     */
    public String optString(String key, String defaultValue) {
        Object o = opt(key);
        if (o != null) {
            return o.toString();
        }
        return defaultValue;
    }


    /**
     * Put a key/boolean pair in the JSONObject.
     *
     * @param key   A key string.
     * @param value A boolean which is the value.
     * @return this.
     */
    public JSONObject put(String key, boolean value) {
        put(key, new Boolean(value));
        return this;
    }


    /**
     * Put a key/double pair in the JSONObject.
     *
     * @param key   A key string.
     * @param value A double which is the value.
     * @return this.
     */
    public JSONObject put(String key, double value) {
        put(key, new Double(value));
        return this;
    }


    /**
     * Put a key/int pair in the JSONObject.
     *
     * @param key   A key string.
     * @param value An int which is the value.
     * @return this.
     */
    public JSONObject put(String key, int value) {
        put(key, new Integer(value));
        return this;
    }


    /**
     * Put a key/value pair in the JSONObject. If the value is null,
     * then the key will be removed from the JSONObject if it is present.
     * @exception NullPointerException The key must be non-null.
     * @param key   A key string.
     * @param value An object which is the value. It should be of one of these
     *  types: Boolean, Double, Integer, JSONArray, JSONObject, String, or the
     *  JSONObject.NULL object.
     * @return this.
     */
    public JSONObject put(String key, Object value) throws NullPointerException {
        if (key == null) {
            throw new NullPointerException("Null key.");
        }
        if (value != null) {
            myHashMap.put(key, value);
        } else {
            remove(key);
        }
        return this;
    }


    /**
     * Put a key/value pair in the JSONObject, but only if the
     * value is non-null.
     * @exception NullPointerException The key must be non-null.
     * @param key   A key string.
     * @param value An object which is the value. It should be of one of these
     *  types: Boolean, Double, Integer, JSONArray, JSONObject, String, or the
     *  JSONObject.NULL object.
     * @return this.
     */
    public JSONObject putOpt(String key, Object value) throws NullPointerException {
        if (value != null) {
            put(key, value);
        }
        return this;
    }


    /**
     * Produce a string in double quotes with backslash sequences in all the
     * right places.
     * @param string A String
     * @return  A String correctly formatted for insertion in a JSON message.
     */
    public static String quote(String string) {
        if (string == null || string.length() == 0) {
            return "\"\"";
        }

        char         c;
        int          i;
        int          len = string.length();
        StringBuffer sb = new StringBuffer(len + 4);
        String       t;

        sb.append('"');
        for (i = 0; i < len; i += 1) {
            c = string.charAt(i);
            switch (c) {
            case '\\':
            case '"':
            case '/':
                sb.append('\\');
                sb.append(c);
                break;
            case '\b':
                sb.append("\\b");
                break;
            case '\t':
                sb.append("\\t");
                break;
            case '\n':
                sb.append("\\n");
                break;
            case '\f':
                sb.append("\\f");
                break;
            case '\r':
                sb.append("\\r");
                break;
            default:
                if (c < ' ' || c >= 128) {
                    t = "000" + Integer.toHexString(c);
                    sb.append("\\u" + t.substring(t.length() - 4));
                } else {
                    sb.append(c);
                }
            }
        }
        sb.append('"');
        return sb.toString();
    }

    /**
     * Remove a name and its value, if present.
     * @param key The name to be removed.
     * @return The value that was associated with the name,
     * or null if there was no value.
     */
    public Object remove(String key) {
        return myHashMap.remove(key);
    }

    /**
     * Produce a JSONArray containing the values of the members of this
     * JSONObject.
     * @param names A JSONArray containing a list of key strings. This
     * determines the sequence of the values in the result.
     * @return A JSONArray of values.
     */
    public JSONArray toJSONArray(JSONArray names) {
        if (names == null || names.length() == 0) {
            return null;
        }
        JSONArray ja = new JSONArray();
        for (int i = 0; i < names.length(); i += 1) {
            ja.put(this.opt(names.getString(i)));
        }
        return ja;
    }

    /**
     * Make an JSON external form string of this JSONObject. For compactness, no
     * unnecessary whitespace is added.
     * <p>
     * Warning: This method assumes that the data structure is acyclical.
     *
     * @return a printable, displayable, portable, transmittable
     *  representation of the object, beginning with '{' and ending with '}'.
     */
    public String toString() {
        Iterator     keys = keys();
        Object       o = null;
        String       s;
        StringBuffer sb = new StringBuffer();

        sb.append('{');
        while (keys.hasNext()) {
            if (o != null) {
                sb.append(',');
            }
            s = keys.next().toString();
            o = myHashMap.get(s);
            if (o != null) {
                sb.append(quote(s));
                sb.append(':');
                if (o instanceof String) {
                    sb.append(quote((String)o));
                } else if (o instanceof Number) {
                    sb.append(numberToString((Number)o));
                } else {
                    sb.append(o.toString());
                }
            }
        }
        sb.append('}');
        return sb.toString();
    }


    /**
     * Make a prettyprinted JSON external form string of this JSONObject.
     * <p>
     * Warning: This method assumes that the data structure is acyclical.
     * @param indentFactor The number of spaces to add to each level of
     *  indentation.
     * @return a printable, displayable, portable, transmittable
     *  representation of the object, beginning with '{' and ending with '}'.
     */
    public String toString(int indentFactor) {
        return toString(indentFactor, 0);
    }


    /**
     * Make a prettyprinted JSON string of this JSONObject.
     * <p>
     * Warning: This method assumes that the data structure is acyclical.
     * @param indentFactor The number of spaces to add to each level of
     *  indentation.
     * @param indent The indentation of the top level.
     * @return a printable, displayable, transmittable
     *  representation of the object, beginning with '{' and ending with '}'.
     */
    String toString(int indentFactor, int indent) {
        int          i;
        Iterator     keys = keys();
        String       pad = "";
        StringBuffer sb = new StringBuffer();
        indent += indentFactor;
        for (i = 0; i < indent; i += 1) {
            pad += ' ';
        }
        sb.append("{\n");
        while (keys.hasNext()) {
            String s = keys.next().toString();
            Object o = myHashMap.get(s);
            if (o != null) {
                if (sb.length() > 2) {
                    sb.append(",\n");
                }
                sb.append(pad);
                sb.append(quote(s));
                sb.append(": ");
                if (o instanceof String) {
                    sb.append(quote((String)o));
                } else if (o instanceof Number) {
                    sb.append(numberToString((Number) o));
                } else if (o instanceof JSONObject) {
                    sb.append(((JSONObject)o).toString(indentFactor, indent));
                } else if (o instanceof JSONArray) {
                    sb.append(((JSONArray)o).toString(indentFactor, indent));
                } else {
                    sb.append(o.toString());
                }
            }
        }
        sb.append('}');
        return sb.toString();
    }
}
